/**
Copyright 2010, David Keyes.  All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may 
not use this file except in compliance with the License. You may obtain
a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0 

Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and 
limitations under the License. 

*/
package com.keyes.license.api.model;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.SortedMap;

import com.keyes.license.Base64Coder;
import com.keyes.protobuf.CodedInputStream;
import com.keyes.protobuf.CodedOutputStream;
import com.keyes.protobuf.WireFormat;

public abstract class ProtoBufBase {
  
  CodedInputStream mCis;
  SortedMap<Integer, ProtoBufMapping> mMappings;
  HashMap<Integer, Object>          mIdMappedValues;
  HashMap<String,  Object>          mNameMappedValues;
  
  public ProtoBufBase(CodedInputStream pCis){
    mCis = pCis;
    mIdMappedValues = new HashMap<Integer, Object>();
    mNameMappedValues = new HashMap<String, Object>();
    mMappings = getMappings();
  }
  
  public ProtoBufBase(){
    this(null);
  }
  
  public void parse() throws IOException {

    if(mCis == null){
      throw new RuntimeException("No CodedInputStream set -- can't parse.");
    }
    
    // derived classes will implement this to define their mappings
    mMappings = this.calculateMappings();
    
    int lTag = mCis.readTag();
    while(lTag != 0){
      ParsedProtoStruct lParsedProtoStruct = new ParsedProtoStruct(lTag);
      
      ProtoBufMapping lMapping = mMappings.get(lParsedProtoStruct.fieldNumber);
      
      // if no mapped field is found, this is an "unknown" field... skip it
      if(lMapping == null){
        mCis.skipField(lTag);
        lTag = mCis.readTag();
        continue;
      }
      
      if      (lMapping.fieldType == PROTOBUFTYPE.VARINT){
        int lValue = mCis.readInt32();
        storeValue(lMapping, lValue);
      } 
      
      else if (lMapping.fieldType == PROTOBUFTYPE.LENGTHDELIM_STRING){
        String lValue = mCis.readString();
        storeValue(lMapping, lValue);
      }

      else if (lMapping.fieldType == PROTOBUFTYPE.START_GROUP || lMapping.fieldType == PROTOBUFTYPE.END_GROUP ){
        // don't process these elements at all
      }

      else if (lMapping.fieldType == PROTOBUFTYPE.LENGTHDELIM_EMBEDDED_MSG){
        Constructor lConst = null;
        int         lLength;
        byte[]      lBytes;
        
        lLength = mCis.readInt32();
        lBytes = mCis.readRawBytes(lLength);
        
        if(lMapping.javaClass != null && ProtoBufBase.class.isAssignableFrom(lMapping.javaClass)){
          try {
            lConst = lMapping.javaClass.getConstructor(CodedInputStream.class);
            
            CodedInputStream lNextCodedInputStream = CodedInputStream.newInstance(lBytes);
            
            ProtoBufBase lNestedProtoBuf = (ProtoBufBase) lConst.newInstance(lNextCodedInputStream);
            
            lNestedProtoBuf.parse();
            
            storeValue(lMapping, lNestedProtoBuf);
            
          } catch(NoSuchMethodException e){
            e.printStackTrace();
          } catch (IllegalArgumentException e) {
            e.printStackTrace();
          } catch (InstantiationException e) {
            e.printStackTrace();
          } catch (IllegalAccessException e) {
            e.printStackTrace();
          } catch (InvocationTargetException e) {
            e.printStackTrace();
          }
        }
       
      }
      
      /////////////////
      // read the next tag
      lTag = mCis.readTag();


    }

  
  }
  
  public String toBase64String() throws IOException {
    return new String(Base64Coder.encode(toBytes()));
  }
  
  public byte[] toBytes() throws IOException {
    ByteArrayOutputStream lBaos = new ByteArrayOutputStream(2048);
    CodedOutputStream lCos = CodedOutputStream.newInstance(lBaos);
    
    byte[] lBytes = toBytes(lCos);
    lBaos.close();
    
    return lBytes;
  }
  
  public byte[] toBytes(CodedOutputStream pCos) throws IOException {
    
    byte[] lRet;
    ProtoBufMapping lCurrentGroupMapping = null;
    
    // order the fields
    
    // this iterator should return the mappings in sorted order based on field id
    Iterator<ProtoBufMapping> lIter = mMappings.values().iterator();
    
    while(lIter.hasNext()){
      
      ProtoBufMapping lMapping = lIter.next();
      
      Object lValue = mIdMappedValues.get(lMapping.fieldId);
      if(lValue == null 
          && lMapping.fieldDefault == null 
          && lMapping.fieldType != PROTOBUFTYPE.START_GROUP
      ){
        continue;
      }
      if(lValue == null && lMapping.fieldDefault != null){
        lValue = lMapping.fieldDefault;
      }
     
      if(lMapping.fieldType == PROTOBUFTYPE.START_GROUP && lCurrentGroupMapping == null){
        lCurrentGroupMapping = lMapping;
        
        pCos.writeTag(lMapping.fieldId, 3 /*START_GROUP*/);
      }
      
      else if(lMapping.fieldType == PROTOBUFTYPE.VARINT){
        pCos.writeInt32(lMapping.fieldId, ((Integer)lValue).intValue());
      }
      
      else if(lMapping.fieldType == PROTOBUFTYPE.LENGTHDELIM_STRING) {
        pCos.writeString(lMapping.fieldId, ((String)lValue));
      }
      
      else if(lMapping.fieldType == PROTOBUFTYPE.LENGTHDELIM_EMBEDDED_MSG) {
        byte[] lSubBytes = ((ProtoBufBase) lValue).toBytes();
        pCos.writeTag(lMapping.fieldId, WireFormat.FieldType.MESSAGE.getWireType());
        pCos.writeRawVarint32(lSubBytes.length);
        pCos.writeRawBytes(lSubBytes);
      }
      

      // if we've reached the end of a group, write the end group tag
      if( (lMapping != lCurrentGroupMapping 
           && lCurrentGroupMapping != null 
           && ! lCurrentGroupMapping.isSubordinateMapping(lMapping)) 
        ||
          (lIter.hasNext() == false 
           && lCurrentGroupMapping != null)
           
      ){
        pCos.writeTag(lCurrentGroupMapping.fieldId, 4 /*end group*/);
      }
      

    }
    
    pCos.flush();
    
    return ((ByteArrayOutputStream)pCos.getOutputStream()).toByteArray();
  }
  
  protected void storeValue(int pFieldId, Object pValue){
    ProtoBufMapping lMapping = mMappings.get(pFieldId);
    
    if(lMapping == null){
      throw new NoSuchElementException();
    }
    
    storeValue(lMapping, pValue);
  }
  
  protected void storeValue(ProtoBufMapping pMapping, Object pValue) {
    mIdMappedValues.put(pMapping.fieldId, pValue);
    mNameMappedValues.put(pMapping.fieldName, pValue);
  }

  public abstract SortedMap<Integer, ProtoBufMapping> calculateMappings();
  
  public SortedMap<Integer, ProtoBufMapping> getMappings(){
    if(mMappings == null){
      mMappings = calculateMappings();
    }
    
    return mMappings;
  }
  
  public static class ProtoBufMapping implements Comparable<ProtoBufMapping> {
    public int          fieldId;
    public String       fieldName;
    public PROTOBUFTYPE fieldType;
    public Class        javaClass;
    public Object       fieldDefault;
    public boolean      required=false;
    public List<ProtoBufMapping> subordinateMappings = new ArrayList<ProtoBufMapping>();
    
    public ProtoBufMapping(int pFieldId, String pFieldName, PROTOBUFTYPE pFieldType, Class pClass, Object pDefault){
      fieldId = pFieldId;
      fieldName = pFieldName;
      fieldType = pFieldType;
      javaClass = pClass;
      fieldDefault = pDefault;
    }
    public ProtoBufMapping(int pFieldId, String pFieldName, PROTOBUFTYPE pFieldType, Class pClass){
      fieldId = pFieldId;
      fieldName = pFieldName;
      fieldType = pFieldType;
      javaClass = pClass;
    }
    public ProtoBufMapping(int pFieldId, String pFieldName, PROTOBUFTYPE pFieldType, boolean pRequired){
      fieldId = pFieldId;
      fieldName = pFieldName;
      fieldType = pFieldType;
      required  = pRequired;
    }
  
    public ProtoBufMapping(int pFieldId, String pFieldName, PROTOBUFTYPE pFieldType, Object pDefault, boolean pRequired){
      fieldId = pFieldId;
      fieldName = pFieldName;
      fieldType = pFieldType;
      fieldDefault = pDefault;
      required = pRequired;
    }
    
    public void setSubordinateMappings(List<ProtoBufMapping> pSubMappings){
      subordinateMappings = pSubMappings;
    }
    
    public void addSubordinteMapping(ProtoBufMapping ...pMappings){
     
      if(pMappings == null){
        return;
      }
      
      for(int i=0; i<pMappings.length; i++){
        subordinateMappings.add(pMappings[i]);
      }
    }
    
    public boolean isSubordinateMapping(ProtoBufMapping pMapping){
      return subordinateMappings.contains(pMapping);
    }
    
    @Override
    public int compareTo(ProtoBufMapping pO) {
      if(this.fieldId < pO.fieldId)
        return -1;
      
      if(this.fieldId == pO.fieldId)
          return 0;
      
      return 1;
    }

  
  }
  

  public static class ParsedProtoStruct {
    
    public int fieldType;
    public int fieldNumber;
    
    public ParsedProtoStruct(int pRaw){
      fieldNumber = pRaw >>> 3;  // top 29 bits are the field number
      fieldType   = pRaw & 0x7;  // last three bits are the field type
    }
  }

  
  public static enum PROTOBUFTYPE {
    VARINT,
    BIT64,
    LENGTHDELIM_STRING,
    LENGTHDELIM_BYTES,
    LENGTHDELIM_EMBEDDED_MSG,
    LENGTHDELIM_PACKED_REPEATED_FIELDS,
    START_GROUP,
    END_GROUP,
    BIT32
  }
}
